package block

import (
	"github.com/boltdb/bolt"
	"fmt"
	"log"
	"encoding/hex"
	"os"
	"bytes"
	"errors"
	"crypto/ecdsa"
)

// ---------------区块链------------

const (
	DbFile  = "blockchain.db"
	BlocksBucket  = "blocks"
	GenesisConinbaseData = "The Times 03/Jan/2009 Chancellor on brink of second bailout for banks"
)

// 新的区块链定义
type Blockchain struct {
	Tip []byte// tip 为数据库中存储的最后一个块的哈希
	Db *bolt.DB
}

// 修改：创建新的区块链
/*
我们希望它做的事情有：
1.打开一个数据库文件
2.检查文件里面是否已经存储了一个区块链
3.如果已经存储了一个区块链：
(1)创建一个新的 Blockchain 实例
(2)设置 Blockchain 实例的 tip 为数据库中存储的最后一个块的哈希
4.如果没有区块链：
(1)创建创世块
(2)存储到数据库
(3)将创世块哈希保存为最后一个块的哈希
(4)创建一个新的 Blockchain 实例，初始时 tip 指向创世块（tip 有尾部，尖端的意思，在这里 tip 存储的是最后一个块的哈希）
*/
func NewBlockchain(address string) *Blockchain {
	if dbExists() == false {
		fmt.Println("No existing blockchain found.Create one first.")
		os.Exit(1)
	}

	var tip []byte
	// 打开数据库
	db,err := bolt.Open(DbFile,0600,nil)
	if err != nil {
		log.Panic(err)
	}
	// 更新事务
	err = db.Update(func(tx *bolt.Tx) error {
		b := tx.Bucket([]byte(BlocksBucket))
		tip = b.Get([]byte("l"))

		return nil
	})

	if err != nil{
		log.Panic(err)
	}

	bc := Blockchain{tip,db}

	return &bc
}

// 修改：创建区块链：创建一个新的区块链数据库
func CreateBlockchain(address string) *Blockchain  {
	// 检查数据库文件是否存在
	if dbExists() {
		fmt.Println("Blockchain already exists.")

		os.Exit(1)
	}

	// 记录最后一个块的哈希，这里既是后面的创世块的哈希即为最后一个块
	var tip []byte

	cbtx := NewCoinbaseTX(address,GenesisConinbaseData)
	genesis := NewGenesisBlock(cbtx)

	// 打开/创建数据库
	db,err := bolt.Open(DbFile,0600,nil)
	if err != nil {
		log.Println(err)
	}

	/*
		更新事务
	(1)通过db.Update实现数据更新操作。
	(2)该操作会被当做一个事务来进行处理，如果Update()内部的操作返回nil，则事务会被提交，否则事务会回滚。
	*/
	err = db.Update(func(tx *bolt.Tx) error {
		// 创建Bucket
		b,err := tx.CreateBucket([]byte(BlocksBucket))
		if err != nil{
			log.Panic(err)
		}

		// 存储创世块数据
		err = b.Put(genesis.Hash,genesis.Serialize())
		if err != nil {
			log.Panic(err)
		}

		// 存储最后使用块文件编号
		err = b.Put([]byte("l"),genesis.Hash)
		if err != nil{
			log.Panic(err)
		}

		// 将创世块哈希保存为最后一个块的哈希
		tip = genesis.Hash

		return nil
	})

	if err != nil {
		log.Panic(err)
	}

	bc := Blockchain{tip,db}

	return &bc
}



/*
	修改：查找花费输出
这个方法对所有的未花费交易进行迭代，并对它的值进行累加。当累加值大于或等于我们想要传送的值时，它就会停止并返回累加值，
同时返回的还有通过交易 ID 进行分组的输出索引。我们只需取出足够支付的钱就够了。
*/
func (bc *Blockchain)FindSpendableOutputs(pubKeyHash []byte,amount int) (int,map[string][]int) {
	// 接收者地址锁定，实际就是给其他地址转移的币
	unspentOutputs := make(map[string][]int)
	// 一个是发送者地址锁定，这是一个找零。只有当未花费输出超过新交易所需时产生。
	unspentTXs := bc.FindUnspentTransactions(pubKeyHash)
	// 累计数
	accumulated := 0

Work:
	for _,tx := range unspentTXs{
		txID := hex.EncodeToString(tx.ID)

		for outIdx,out := range tx.Vout{
			if out.IsLockedWithKey(pubKeyHash)&&accumulated < amount {
				accumulated += out.Value
				unspentOutputs[txID] = append(unspentOutputs[txID],outIdx)

				if accumulated >= amount {
					break Work
				}
			}
		}
	}

	return accumulated,unspentOutputs
}

// 修改：查找未花费输出的交易
func (bc *Blockchain)FindUnspentTransactions(pubKeyHash []byte)[]Transaction  {
	// 未花费输出的交易
	var unspentTXs []Transaction

	// 已经花费输出的交易
	spentTXOs := make(map[string][]int)
	bci := bc.Iterator()

	for {
		// （1）循环从迭代器里面查找块，即从数据库里面查找块
		block := bci.Next()

		// （2）循环遍历查找到的块里面的交易数据
		for _,tx := range block.Transactions {
/*
	hex包：实现16进制数的编码和解码。

	func EncodeToString(src []byte) string
	返回数据源src的16进制编码的字符串
*/
			txID := hex.EncodeToString(tx.ID)// 处理交易ID

			// 遍历交易输出
			Outputs :
				for outIdx,out := range tx.Vout {
				/*
					注意：
				A.交易被存储到区块里面，因此需要我们检查区块链里的每一笔交易。从输出开:if out.CanBeUnlockedWith(address) {
				unspentTXs = append(unspentTXs, tx)}
				B.如果一个输出被一个地址锁定，并且这个地址恰好是我们要找的地址，那么这个输出就是我们要找的。
				不过在获取它之前，我们需要检查该输出是否已经被包含在一个交易的输入中，也就是检查它是否已经被花费了，
				即:if spentTXOs[txID] != nil {}
				*/
					// 判读是否是花费输出
					if spentTXOs[txID] != nil {
						for _,spentOut := range spentTXOs[txID]{
							if spentOut == outIdx{
								continue Outputs
							}
						}
					}

					if out.IsLockedWithKey(pubKeyHash) {// 集合所有未花费的输出交易数据
						unspentTXs = append(unspentTXs, *tx)
					}
				}

		// （3）检查输出后，将给定地地址所有能够解锁输出的输入聚集起来（这并不适用coinbase交易，因为它们不解锁输出。）
			if tx.IsCoinbase() == false {
				for _,in := range tx.Vin {
					if in.UsesKey(pubKeyHash) {
						inTxID := hex.EncodeToString(in.Txid)
						spentTXOs[inTxID] = append(spentTXOs[inTxID],in.Vout)
					}
				}
			}
		}

		if len(block.PreBlockHash) == 0 {
			break
		}
	}

	return unspentTXs
}

// 修改：(1)返回一个交易输出列表：将交易作为输入，为了计算余额-->转移到cli.go中的getBalance方法中实现计算余额
func (bc *Blockchain)FindUTXO(pubKeyHash []byte)[]TXOutput  {
	var UTXOs []TXOutput

	unspentTransactions := bc.FindUnspentTransactions(pubKeyHash)

	for _,tx := range unspentTransactions {// 遍历访问未花费输出的交易数据
		for _,out := range tx.Vout{
			if out.IsLockedWithKey(pubKeyHash) {
				UTXOs = append(UTXOs,out)
			}
		}
	}

	return UTXOs
}

// 创建迭代器
func (bc *Blockchain)Iterator() *BlockchainIterator {
	bci := &BlockchainIterator{
		bc.Tip,
		bc.Db}

	return bci
}

// 修改创建自己的区块
func (bc *Blockchain)MineBlock(transactions []*Transaction)   {
	var lastHash []byte

	for _,tx := range transactions{
		if bc.VerifyTransaction(tx) != true {
			log.Panic("ERROR:Invalid transaction")
		}
	}

	err := bc.Db.View(func(tx *bolt.Tx) error {
		b := tx.Bucket([]byte(BlocksBucket))
		lastHash = b.Get([]byte("l"))

		return nil
	})

	if err != nil {
		log.Panic(err)
	}

	newBlock := NewBlock(transactions,lastHash)
	err = bc.Db.Update(func(tx *bolt.Tx) error {
		b := tx.Bucket([]byte(BlocksBucket))
		err := b.Put(newBlock.Hash,newBlock.Serialize())
		if err != nil {
			log.Panic(err)
		}

		err = b.Put([]byte("l"),newBlock.Hash)
		if err != nil {
			 log.Panic(err)
		}

		bc.Tip = newBlock.Hash

		return nil
	})

	if err != nil {
		log.Panic(err)
	}
}

// 查找数据库是否存在
func dbExists()bool{
/*
	(1)os包
os包提供了操作系统函数的不依赖平台的接口
	(2)func Stat(name string) (fi FileInfo, err error)
Stat返回一个描述name指定的文件对象的FileInfo。如果指定的文件对象是一个符号链接，
返回的FileInfo描述该符号链接指向的文件的信息，本函数会尝试跳转该链接。
如果出错，返回的错误值为*PathError类型。
*/
	if _,err := os.Stat(DbFile);os.IsNotExist(err) {
		return false
	}

	return true
}

// 六、(1)获取之前交易：通过ID找到一笔交易
func (bc *Blockchain)FindTransaction(ID []byte)(Transaction,error)  {
	bci := bc.Iterator()

	for {
		block := bci.Next()

		for _,tx := range block.Transactions{
			if bytes.Compare(tx.ID,ID) == 0 {
				return  *tx,nil
			}
		}

		if len(block.PreBlockHash) == 0 {
			break
		}
	}
	/*
		func New(text string) error
	使用字符串创建一个错误,请类比fmt包的Errorf方法，差不多可以认为是New(fmt.Sprintf(...))。
	*/
	return Transaction{},errors.New("Transaction is not found")
}

// 六、(2)交易签名：传入一笔交易，找到它引用的交易，然后对它进行签名。
func (bc *Blockchain)SignTransaction(tx *Transaction,privKey ecdsa.PrivateKey)  {
	prevTXs := make(map[string]Transaction)

	for _,vin := range tx.Vin{
		prevTX,err := bc.FindTransaction(vin.Txid)
		if err != nil{
			log.Panic(err)
		}

		prevTXs[hex.EncodeToString(prevTX.ID)] = prevTX
	}

	tx.Sign(privKey,prevTXs)
}

// 六、(3)验证交易:验证交易输入的签名
func (bc *Blockchain)VerifyTransaction(tx *Transaction) bool  {
	prevTXs := make(map[string]Transaction)

	for _,vin := range tx.Vin{
		prevTX,err := bc.FindTransaction(vin.Txid)
		if err != nil {
			log.Panic(err)
		}

		prevTXs[hex.EncodeToString(prevTX.ID)] = prevTX
	}

	return tx.Verify(prevTXs)
}